NexusPi Git Node
flash.py v1.0.8 (4e5d4ee8) Text, 31.41 KB
T8b949e#!/usr/bin/env python3
T8b949e"""
RNodeTHV4 Flash Utility
Flash the RNodeTHV4 boundary node firmware to a Heltec WiFi LoRa 32 V3 or V4.
No PlatformIO required — just Python 3 and a USB cable.
Default mode flashes only the app partition (0x10000), preserving
bootloader, partition table, NVS, and EEPROM settings.
Usage:
# Update firmware — V4 (default)
python flash.py
# Update firmware — V3
python flash.py --board v3
# Full flash with merged binary (overwrites everything)
python flash.py --full
# Flash a specific file (auto-detects merged vs app-only)
python flash.py --file firmware.bin
# Download latest from GitHub and flash
python flash.py --download
# Specify serial port manually
python flash.py --port /dev/ttyACM0
# Just build the merged binary (for GitHub Releases)
python flash.py --merge-only
"""
Tff7b72import T7ee787argparse
Tff7b72import T7ee787glob
Tff7b72import T7ee787hashlib
Tff7b72import T7ee787os
Tff7b72import T7ee787platform
Tff7b72import T7ee787shutil
Tff7b72import T7ee787subprocess
Tff7b72import T7ee787sys
Tff7b72import T7ee787time
T8b949e# ── Configuration ──────────────────────────────────────────────────────────────
Te6edf3CHIP Tff7b72= Ta5d6ff"Ta5d6ffesp32s3Ta5d6ff"
Te6edf3FLASH_MODE Tff7b72= Ta5d6ff"Ta5d6ffqioTa5d6ff"
Te6edf3FLASH_FREQ Tff7b72= Ta5d6ff"Ta5d6ff80mTa5d6ff"
Te6edf3GITHUB_REPO Tff7b72= Ta5d6ff"Ta5d6ffjrl290/RNodeTHV4Ta5d6ff"
T8b949e# Flash addresses for ESP32-S3 Arduino framework
Te6edf3BOOTLOADER_ADDR Tff7b72= T79c0ff0x0000
Te6edf3PARTITIONS_ADDR Tff7b72= T79c0ff0x8000
Te6edf3BOOT_APP0_ADDR Tff7b72= T79c0ff0xe000
Te6edf3APP_ADDR Tff7b72= T79c0ff0x10000
T8b949e# ── Board profiles ─────────────────────────────────────────────────────────────
T8b949e# Each board defines its PIO env, flash size, baud rate, firmware binary name,
T8b949e# and merged binary name.
Te6edf3BOARD_PROFILES Tff7b72= Tb4b4b4{
Ta5d6ff"Ta5d6ffv4Ta5d6ff"Tb4b4b4: Tb4b4b4{
Ta5d6ff"Ta5d6ffnameTa5d6ff"Tb4b4b4: Ta5d6ff"Ta5d6ffHeltec WiFi LoRa 32 V4Ta5d6ff"Tb4b4b4,
Ta5d6ff"Ta5d6ffpio_envTa5d6ff"Tb4b4b4: Ta5d6ff"Ta5d6ffheltec_V4_boundaryTa5d6ff"Tb4b4b4,
Ta5d6ff"Ta5d6ffbuild_dirTa5d6ff"Tb4b4b4: Ta5d6ff"Ta5d6ff.pio/build/heltec_V4_boundaryTa5d6ff"Tb4b4b4,
Ta5d6ff"Ta5d6fffirmware_binTa5d6ff"Tb4b4b4: Ta5d6ff"Ta5d6ffrnode_firmware_heltec32v4_boundary.binTa5d6ff"Tb4b4b4,
Ta5d6ff"Ta5d6ffmerged_filenameTa5d6ff"Tb4b4b4: Ta5d6ff"Ta5d6ffrnodethv4_firmware.binTa5d6ff"Tb4b4b4,
Ta5d6ff"Ta5d6ffflash_sizeTa5d6ff"Tb4b4b4: Ta5d6ff"Ta5d6ff16MBTa5d6ff"Tb4b4b4,
Ta5d6ff"Ta5d6ffbaud_rateTa5d6ff"Tb4b4b4: Ta5d6ff"Ta5d6ff921600Ta5d6ff"Tb4b4b4,
Tb4b4b4}Tb4b4b4,
Ta5d6ff"Ta5d6ffv3Ta5d6ff"Tb4b4b4: Tb4b4b4{
Ta5d6ff"Ta5d6ffnameTa5d6ff"Tb4b4b4: Ta5d6ff"Ta5d6ffHeltec WiFi LoRa 32 V3Ta5d6ff"Tb4b4b4,
Ta5d6ff"Ta5d6ffpio_envTa5d6ff"Tb4b4b4: Ta5d6ff"Ta5d6ffheltec_V3_boundaryTa5d6ff"Tb4b4b4,
Ta5d6ff"Ta5d6ffbuild_dirTa5d6ff"Tb4b4b4: Ta5d6ff"Ta5d6ff.pio/build/heltec_V3_boundaryTa5d6ff"Tb4b4b4,
Ta5d6ff"Ta5d6fffirmware_binTa5d6ff"Tb4b4b4: Ta5d6ff"Ta5d6ffrnode_firmware_heltec32v3.binTa5d6ff"Tb4b4b4,
Ta5d6ff"Ta5d6ffmerged_filenameTa5d6ff"Tb4b4b4: Ta5d6ff"Ta5d6ffrnodethv3_firmware.binTa5d6ff"Tb4b4b4,
Ta5d6ff"Ta5d6ffflash_sizeTa5d6ff"Tb4b4b4: Ta5d6ff"Ta5d6ff8MBTa5d6ff"Tb4b4b4,
Ta5d6ff"Ta5d6ffbaud_rateTa5d6ff"Tb4b4b4: Ta5d6ff"Ta5d6ff460800Ta5d6ff"Tb4b4b4,
Tb4b4b4}Tb4b4b4,
Tb4b4b4}
Te6edf3DEFAULT_BOARD Tff7b72= Ta5d6ff"Ta5d6ffv4Ta5d6ff"
T8b949e# Active board profile (set in main() from --board arg)
Te6edf3_board Tff7b72= Tff7b72None
Tff7b72def Td2a8ffboard_profileTb4b4b4(Tb4b4b4)Tb4b4b4:
Tff7b72return Te6edf3BOARD_PROFILESTb4b4b4[Te6edf3_board Tff7b72or Te6edf3DEFAULT_BOARDTb4b4b4]
Tff7b72def Td2a8ffBUILD_DIRTb4b4b4(Tb4b4b4)Tb4b4b4:
Tff7b72return Te6edf3board_profileTb4b4b4(Tb4b4b4)Tb4b4b4[Ta5d6ff"Ta5d6ffbuild_dirTa5d6ff"Tb4b4b4]
Tff7b72def Td2a8ffBOOTLOADER_BINTb4b4b4(Tb4b4b4)Tb4b4b4:
Tff7b72return Te6edf3osTff7b72.Td2a8ffpathTff7b72.Td2a8ffjoinTb4b4b4(Te6edf3BUILD_DIRTb4b4b4(Tb4b4b4)Tb4b4b4, Ta5d6ff"Ta5d6ffbootloader.binTa5d6ff"Tb4b4b4)
Tff7b72def Td2a8ffPARTITIONS_BINTb4b4b4(Tb4b4b4)Tb4b4b4:
Tff7b72return Te6edf3osTff7b72.Td2a8ffpathTff7b72.Td2a8ffjoinTb4b4b4(Te6edf3BUILD_DIRTb4b4b4(Tb4b4b4)Tb4b4b4, Ta5d6ff"Ta5d6ffpartitions.binTa5d6ff"Tb4b4b4)
Tff7b72def Td2a8ffFIRMWARE_BINTb4b4b4(Tb4b4b4)Tb4b4b4:
Tff7b72return Te6edf3osTff7b72.Td2a8ffpathTff7b72.Td2a8ffjoinTb4b4b4(Te6edf3BUILD_DIRTb4b4b4(Tb4b4b4)Tb4b4b4, Te6edf3board_profileTb4b4b4(Tb4b4b4)Tb4b4b4[Ta5d6ff"Ta5d6fffirmware_binTa5d6ff"Tb4b4b4]Tb4b4b4)
Tff7b72def Td2a8ffFLASH_SIZETb4b4b4(Tb4b4b4)Tb4b4b4:
Tff7b72return Te6edf3board_profileTb4b4b4(Tb4b4b4)Tb4b4b4[Ta5d6ff"Ta5d6ffflash_sizeTa5d6ff"Tb4b4b4]
Tff7b72def Td2a8ffBAUD_RATETb4b4b4(Tb4b4b4)Tb4b4b4:
Tff7b72return Te6edf3board_profileTb4b4b4(Tb4b4b4)Tb4b4b4[Ta5d6ff"Ta5d6ffbaud_rateTa5d6ff"Tb4b4b4]
Tff7b72def Td2a8ffMERGED_FILENAMETb4b4b4(Tb4b4b4)Tb4b4b4:
Tff7b72return Te6edf3board_profileTb4b4b4(Tb4b4b4)Tb4b4b4[Ta5d6ff"Ta5d6ffmerged_filenameTa5d6ff"Tb4b4b4]
Tff7b72def Td2a8ffPIO_ENVTb4b4b4(Tb4b4b4)Tb4b4b4:
Tff7b72return Te6edf3board_profileTb4b4b4(Tb4b4b4)Tb4b4b4[Ta5d6ff"Ta5d6ffpio_envTa5d6ff"Tb4b4b4]
T8b949e# ESP32 partition table magic bytes (first two bytes of a partition table entry)
Te6edf3PARTITION_TABLE_MAGIC Tff7b72= Ta5d6ffbTa5d6ff'Tffea00\xaaTffea00\x50Ta5d6ff'
Tff7b72def Td2a8ffis_merged_binaryTb4b4b4(Te6edf3firmware_pathTb4b4b4)Tb4b4b4:
T8b949e"""Check whether a firmware file is a merged binary (contains bootloader +
partition table) or an app-only binary.
Returns True for merged, False for app-only.
"""
Tff7b72tryTb4b4b4:
Te6edf3size Tff7b72= Te6edf3osTff7b72.Td2a8ffpathTff7b72.Td2a8ffgetsizeTb4b4b4(Te6edf3firmware_pathTb4b4b4)
Tff7b72if Te6edf3size Tff7b72> T79c0ff0x8002Tb4b4b4:
Tff7b72with Tffa657openTb4b4b4(Te6edf3firmware_pathTb4b4b4, Ta5d6ff"Ta5d6ffrbTa5d6ff"Tb4b4b4) Tff7b72as Te6edf3fTb4b4b4:
Te6edf3fTff7b72.Td2a8ffseekTb4b4b4(T79c0ff0x8000Tb4b4b4)
Tff7b72return Te6edf3fTff7b72.Td2a8ffreadTb4b4b4(T79c0ff2Tb4b4b4) Tff7b72== Te6edf3PARTITION_TABLE_MAGIC
Tff7b72except Tf85149ExceptionTb4b4b4:
Tff7b72pass
Tff7b72return Tff7b72False
Tff7b72def Td2a8ff_find_in_platformio_or_releaseTb4b4b4(Te6edf3build_pathTb4b4b4, Te6edf3release_nameTb4b4b4)Tb4b4b4:
T8b949e"""Find a file in the PlatformIO build output or the bundled Release/ dir."""
T8b949e# 1. PlatformIO build output
Tff7b72if Te6edf3osTff7b72.Td2a8ffpathTff7b72.Td2a8ffisfileTb4b4b4(Te6edf3build_pathTb4b4b4)Tb4b4b4:
Tff7b72return Te6edf3build_path
T8b949e# 2. Bundled in Release/
Te6edf3bundled Tff7b72= Te6edf3osTff7b72.Td2a8ffpathTff7b72.Td2a8ffjoinTb4b4b4(Te6edf3osTff7b72.Td2a8ffpathTff7b72.Td2a8ffdirnameTb4b4b4(Tff7b72__file__Tb4b4b4)Tb4b4b4, Ta5d6ff"Ta5d6ffReleaseTa5d6ff"Tb4b4b4, Te6edf3release_nameTb4b4b4)
Tff7b72if Te6edf3osTff7b72.Td2a8ffpathTff7b72.Td2a8ffisfileTb4b4b4(Te6edf3bundledTb4b4b4)Tb4b4b4:
Tff7b72return Te6edf3bundled
Tff7b72return Tff7b72None
T8b949e# Forward-compatible aliases (these are now functions, not constants)
Tff7b72def Td2a8ff_bootloader_binTb4b4b4(Tb4b4b4)Tb4b4b4:
Tff7b72return Te6edf3BOOTLOADER_BINTb4b4b4(Tb4b4b4)
Tff7b72def Td2a8ff_partitions_binTb4b4b4(Tb4b4b4)Tb4b4b4:
Tff7b72return Te6edf3PARTITIONS_BINTb4b4b4(Tb4b4b4)
Tff7b72def Td2a8ff_firmware_binTb4b4b4(Tb4b4b4)Tb4b4b4:
Tff7b72return Te6edf3FIRMWARE_BINTb4b4b4(Tb4b4b4)
Tff7b72def Td2a8fffind_boot_app0Tb4b4b4(Tb4b4b4)Tb4b4b4:
T8b949e"""Find boot_app0.bin from PlatformIO framework packages.
Handles versioned package directories (e.g. framework-arduinoespressif32@3.20009.0).
"""
Te6edf3pio_dir Tff7b72= Te6edf3osTff7b72.Td2a8ffpathTff7b72.Td2a8ffexpanduserTb4b4b4(Ta5d6ff"Ta5d6ff~/.platformio/packagesTa5d6ff"Tb4b4b4)
T8b949e# Try exact name first
Te6edf3exact Tff7b72= Te6edf3osTff7b72.Td2a8ffpathTff7b72.Td2a8ffjoinTb4b4b4(Te6edf3pio_dirTb4b4b4, Ta5d6ff"Ta5d6ffframework-arduinoespressif32Ta5d6ff"Tb4b4b4,
Ta5d6ff"Ta5d6fftoolsTa5d6ff"Tb4b4b4, Ta5d6ff"Ta5d6ffpartitionsTa5d6ff"Tb4b4b4, Ta5d6ff"Ta5d6ffboot_app0.binTa5d6ff"Tb4b4b4)
Tff7b72if Te6edf3osTff7b72.Td2a8ffpathTff7b72.Td2a8ffisfileTb4b4b4(Te6edf3exactTb4b4b4)Tb4b4b4:
Tff7b72return Te6edf3exact
T8b949e# Try versioned directories
Tff7b72if Te6edf3osTff7b72.Td2a8ffpathTff7b72.Td2a8ffisdirTb4b4b4(Te6edf3pio_dirTb4b4b4)Tb4b4b4:
Tff7b72for Te6edf3name Tff7b72in Tffa657sortedTb4b4b4(Te6edf3osTff7b72.Td2a8fflistdirTb4b4b4(Te6edf3pio_dirTb4b4b4)Tb4b4b4, Te6edf3reverseTff7b72=Tff7b72TrueTb4b4b4)Tb4b4b4:
Tff7b72if Te6edf3nameTff7b72.Td2a8ffstartswithTb4b4b4(Ta5d6ff"Ta5d6ffframework-arduinoespressif32Ta5d6ff"Tb4b4b4)Tb4b4b4:
Te6edf3candidate Tff7b72= Te6edf3osTff7b72.Td2a8ffpathTff7b72.Td2a8ffjoinTb4b4b4(Te6edf3pio_dirTb4b4b4, Te6edf3nameTb4b4b4, Ta5d6ff"Ta5d6fftoolsTa5d6ff"Tb4b4b4, Ta5d6ff"Ta5d6ffpartitionsTa5d6ff"Tb4b4b4, Ta5d6ff"Ta5d6ffboot_app0.binTa5d6ff"Tb4b4b4)
Tff7b72if Te6edf3osTff7b72.Td2a8ffpathTff7b72.Td2a8ffisfileTb4b4b4(Te6edf3candidateTb4b4b4)Tb4b4b4:
Tff7b72return Te6edf3candidate
T8b949e# Bundled fallback
Te6edf3bundled Tff7b72= Te6edf3osTff7b72.Td2a8ffpathTff7b72.Td2a8ffjoinTb4b4b4(Te6edf3osTff7b72.Td2a8ffpathTff7b72.Td2a8ffdirnameTb4b4b4(Tff7b72__file__Tb4b4b4)Tb4b4b4, Ta5d6ff"Ta5d6ffReleaseTa5d6ff"Tb4b4b4, Ta5d6ff"Ta5d6ffboot_app0.binTa5d6ff"Tb4b4b4)
Tff7b72if Te6edf3osTff7b72.Td2a8ffpathTff7b72.Td2a8ffisfileTb4b4b4(Te6edf3bundledTb4b4b4)Tb4b4b4:
Tff7b72return Te6edf3bundled
Tff7b72return Tff7b72None
Tff7b72def Td2a8fffind_bootloaderTb4b4b4(Tb4b4b4)Tb4b4b4:
T8b949e"""Find bootloader.bin from PlatformIO build output or Release/ bundle."""
Tff7b72return Te6edf3_find_in_platformio_or_releaseTb4b4b4(Te6edf3BOOTLOADER_BINTb4b4b4(Tb4b4b4)Tb4b4b4, Ta5d6ff"Ta5d6ffbootloader.binTa5d6ff"Tb4b4b4)
Tff7b72def Td2a8fffind_partitionsTb4b4b4(Tb4b4b4)Tb4b4b4:
T8b949e"""Find partitions.bin from PlatformIO build output or Release/ bundle."""
Tff7b72return Te6edf3_find_in_platformio_or_releaseTb4b4b4(Te6edf3PARTITIONS_BINTb4b4b4(Tb4b4b4)Tb4b4b4, Ta5d6ff"Ta5d6ffpartitions.binTa5d6ff"Tb4b4b4)
Te6edf3BOOT_APP0_BIN Tff7b72= Te6edf3find_boot_app0Tb4b4b4(Tb4b4b4)
T8b949e# ── Board auto-detection ───────────────────────────────────────────────────────
T8b949e# Map detected flash sizes to board keys
Te6edf3_FLASH_SIZE_TO_BOARD Tff7b72= Tb4b4b4{
Ta5d6ff"Ta5d6ff16MBTa5d6ff"Tb4b4b4: Ta5d6ff"Ta5d6ffv4Ta5d6ff"Tb4b4b4,
Ta5d6ff"Ta5d6ff8MBTa5d6ff"Tb4b4b4: Ta5d6ff"Ta5d6ffv3Ta5d6ff"Tb4b4b4,
Tb4b4b4}
Tff7b72def Td2a8ffdetect_boardTb4b4b4(Te6edf3portTb4b4b4, Te6edf3esptool_cmdTb4b4b4)Tb4b4b4:
T8b949e"""Auto-detect which Heltec board is connected by querying flash size.
Runs ``esptool.py flash_id`` and parses the output for:
- Detected flash size (16MB → V4, 8MB → V3)
- Chip type (ESP32-S3 expected)
- Features (PSRAM size, WiFi, BLE)
Returns a tuple (board_key, info_dict) on success, or (None, reason) on
failure. ``board_key`` is "v3" or "v4".
"""
Te6edf3cmd Tff7b72= Te6edf3esptool_cmd Tff7b72+ Tb4b4b4[Ta5d6ff"Ta5d6ff--portTa5d6ff"Tb4b4b4, Te6edf3portTb4b4b4, Ta5d6ff"Ta5d6ffflash_idTa5d6ff"Tb4b4b4]
Tff7b72tryTb4b4b4:
Te6edf3result Tff7b72= Te6edf3subprocessTff7b72.Td2a8ffrunTb4b4b4(Te6edf3cmdTb4b4b4, Te6edf3capture_outputTff7b72=Tff7b72TrueTb4b4b4, Te6edf3textTff7b72=Tff7b72TrueTb4b4b4, Te6edf3timeoutTff7b72=T79c0ff15Tb4b4b4)
Tff7b72except Te6edf3subprocessTff7b72.Td2a8ffTimeoutExpiredTb4b4b4:
Tff7b72return Tff7b72NoneTb4b4b4, Ta5d6ff"Ta5d6ffesptool timed out (device not responding?)Ta5d6ff"
Tff7b72except Tf85149Exception Tff7b72as Te6edf3eTb4b4b4:
Tff7b72return Tff7b72NoneTb4b4b4, Tffa657strTb4b4b4(Te6edf3eTb4b4b4)
Te6edf3output Tff7b72= Te6edf3resultTff7b72.Td2a8ffstdout Tff7b72+ Te6edf3resultTff7b72.Td2a8ffstderr
Tff7b72if Te6edf3resultTff7b72.Td2a8ffreturncode Tff7b72!= T79c0ff0Tb4b4b4:
Tff7b72return Tff7b72NoneTb4b4b4, Ta5d6fffTa5d6ff"Ta5d6ffesptool flash_id failed:Tffea00\nTffd700{Te6edf3outputTff7b72.Td2a8ffstripTb4b4b4(Tb4b4b4)Tffd700}Ta5d6ff"
T8b949e# Parse key fields
Te6edf3info Tff7b72= Tb4b4b4{Tb4b4b4}
Tff7b72for Te6edf3line Tff7b72in Te6edf3outputTff7b72.Td2a8ffsplitlinesTb4b4b4(Tb4b4b4)Tb4b4b4:
Te6edf3line Tff7b72= Te6edf3lineTff7b72.Td2a8ffstripTb4b4b4(Tb4b4b4)
Tff7b72if Te6edf3lineTff7b72.Td2a8ffstartswithTb4b4b4(Ta5d6ff"Ta5d6ffChip is Ta5d6ff"Tb4b4b4)Tb4b4b4:
Te6edf3infoTb4b4b4[Ta5d6ff"Ta5d6ffchipTa5d6ff"Tb4b4b4] Tff7b72= Te6edf3lineTb4b4b4[Tffa657lenTb4b4b4(Ta5d6ff"Ta5d6ffChip is Ta5d6ff"Tb4b4b4)Tb4b4b4:Tb4b4b4]
Tff7b72elif Te6edf3lineTff7b72.Td2a8ffstartswithTb4b4b4(Ta5d6ff"Ta5d6ffFeatures:Ta5d6ff"Tb4b4b4)Tb4b4b4:
Te6edf3infoTb4b4b4[Ta5d6ff"Ta5d6fffeaturesTa5d6ff"Tb4b4b4] Tff7b72= Te6edf3lineTb4b4b4[Tffa657lenTb4b4b4(Ta5d6ff"Ta5d6ffFeatures:Ta5d6ff"Tb4b4b4)Tb4b4b4:Tb4b4b4]Tff7b72.Td2a8ffstripTb4b4b4(Tb4b4b4)
Tff7b72elif Te6edf3lineTff7b72.Td2a8ffstartswithTb4b4b4(Ta5d6ff"Ta5d6ffDetected flash size:Ta5d6ff"Tb4b4b4)Tb4b4b4:
Te6edf3infoTb4b4b4[Ta5d6ff"Ta5d6ffflash_sizeTa5d6ff"Tb4b4b4] Tff7b72= Te6edf3lineTff7b72.Td2a8ffsplitTb4b4b4(Ta5d6ff"Ta5d6ff:Ta5d6ff"Tb4b4b4)Tb4b4b4[Tff7b72-T79c0ff1Tb4b4b4]Tff7b72.Td2a8ffstripTb4b4b4(Tb4b4b4)
Tff7b72elif Te6edf3lineTff7b72.Td2a8ffstartswithTb4b4b4(Ta5d6ff"Ta5d6ffMAC:Ta5d6ff"Tb4b4b4)Tb4b4b4:
Te6edf3infoTb4b4b4[Ta5d6ff"Ta5d6ffmacTa5d6ff"Tb4b4b4] Tff7b72= Te6edf3lineTff7b72.Td2a8ffsplitTb4b4b4(Ta5d6ff"Ta5d6ff:Ta5d6ff"Tb4b4b4)Tb4b4b4[Tff7b72-T79c0ff5Tb4b4b4:Tb4b4b4] T8b949e# last 5 colon-groups
Te6edf3infoTb4b4b4[Ta5d6ff"Ta5d6ffmacTa5d6ff"Tb4b4b4] Tff7b72= Te6edf3lineTb4b4b4[Tffa657lenTb4b4b4(Ta5d6ff"Ta5d6ffMAC:Ta5d6ff"Tb4b4b4)Tb4b4b4:Tb4b4b4]Tff7b72.Td2a8ffstripTb4b4b4(Tb4b4b4)
Tff7b72elif Te6edf3lineTff7b72.Td2a8ffstartswithTb4b4b4(Ta5d6ff"Ta5d6ffCrystal isTa5d6ff"Tb4b4b4)Tb4b4b4:
Te6edf3infoTb4b4b4[Ta5d6ff"Ta5d6ffcrystalTa5d6ff"Tb4b4b4] Tff7b72= Te6edf3lineTb4b4b4[Tffa657lenTb4b4b4(Ta5d6ff"Ta5d6ffCrystal isTa5d6ff"Tb4b4b4)Tb4b4b4:Tb4b4b4]Tff7b72.Td2a8ffstripTb4b4b4(Tb4b4b4)
Te6edf3flash_size Tff7b72= Te6edf3infoTff7b72.Td2a8ffgetTb4b4b4(Ta5d6ff"Ta5d6ffflash_sizeTa5d6ff"Tb4b4b4)
Tff7b72if Tff7b72not Te6edf3flash_sizeTb4b4b4:
Tff7b72return Tff7b72NoneTb4b4b4, Ta5d6fffTa5d6ff"Ta5d6ffCould not parse flash size from esptool output:Tffea00\nTffd700{Te6edf3outputTff7b72.Td2a8ffstripTb4b4b4(Tb4b4b4)Tffd700}Ta5d6ff"
Te6edf3board_key Tff7b72= Te6edf3_FLASH_SIZE_TO_BOARDTff7b72.Td2a8ffgetTb4b4b4(Te6edf3flash_sizeTb4b4b4)
Tff7b72if Tff7b72not Te6edf3board_keyTb4b4b4:
Tff7b72return Tff7b72NoneTb4b4b4, Tb4b4b4(
Ta5d6fffTa5d6ff"Ta5d6ffUnknown flash size Ta5d6ff'Tffd700{Te6edf3flash_sizeTffd700}Ta5d6ff'Ta5d6ff — expected 16MB (V4) or 8MB (V3).Tffea00\nTa5d6ff"
Ta5d6fffTa5d6ff"Ta5d6ffUse --board v3 or --board v4 to specify manually.Ta5d6ff"
Tb4b4b4)
Tff7b72return Te6edf3board_keyTb4b4b4, Te6edf3info
T8b949e# ── Helpers ────────────────────────────────────────────────────────────────────
Tff7b72def Td2a8fffind_esptoolTb4b4b4(Tb4b4b4)Tb4b4b4:
T8b949e"""Find esptool.py — pip-installed, bundled, or PlatformIO's copy.
Prefer pip-installed esptool first (handles its own deps), then fall
back to the bundled script — but only if pyserial is importable in
the current Python interpreter.
"""
T8b949e# 1. pip-installed esptool (standalone executable, no dep issues)
Tff7b72if Te6edf3shutilTff7b72.Td2a8ffwhichTb4b4b4(Ta5d6ff"Ta5d6ffesptool.pyTa5d6ff"Tb4b4b4)Tb4b4b4:
Tff7b72return Tb4b4b4[Ta5d6ff"Ta5d6ffesptool.pyTa5d6ff"Tb4b4b4]
Tff7b72if Te6edf3shutilTff7b72.Td2a8ffwhichTb4b4b4(Ta5d6ff"Ta5d6ffesptoolTa5d6ff"Tb4b4b4)Tb4b4b4:
Tff7b72return Tb4b4b4[Ta5d6ff"Ta5d6ffesptoolTa5d6ff"Tb4b4b4]
T8b949e# Check if pyserial is available before using script-based esptool
Tff7b72tryTb4b4b4:
Tff7b72import T7ee787serial T8b949e# noqa: F401
Te6edf3has_pyserial Tff7b72= Tff7b72True
Tff7b72except Tf85149ImportErrorTb4b4b4:
Te6edf3has_pyserial Tff7b72= Tff7b72False
T8b949e# 2. Bundled in Release/
Te6edf3bundled Tff7b72= Te6edf3osTff7b72.Td2a8ffpathTff7b72.Td2a8ffjoinTb4b4b4(Te6edf3osTff7b72.Td2a8ffpathTff7b72.Td2a8ffdirnameTb4b4b4(Tff7b72__file__Tb4b4b4)Tb4b4b4, Ta5d6ff"Ta5d6ffReleaseTa5d6ff"Tb4b4b4, Ta5d6ff"Ta5d6ffesptoolTa5d6ff"Tb4b4b4, Ta5d6ff"Ta5d6ffesptool.pyTa5d6ff"Tb4b4b4)
Tff7b72if Te6edf3osTff7b72.Td2a8ffpathTff7b72.Td2a8ffisfileTb4b4b4(Te6edf3bundledTb4b4b4) Tff7b72and Te6edf3has_pyserialTb4b4b4:
Tff7b72return Tb4b4b4[Te6edf3sysTff7b72.Td2a8ffexecutableTb4b4b4, Te6edf3bundledTb4b4b4]
T8b949e# 3. PlatformIO's esptool
Te6edf3pio_esptool Tff7b72= Te6edf3osTff7b72.Td2a8ffpathTff7b72.Td2a8ffexpanduserTb4b4b4(
Ta5d6ff"Ta5d6ff~/.platformio/packages/tool-esptoolpy/esptool.pyTa5d6ff"
Tb4b4b4)
Tff7b72if Te6edf3osTff7b72.Td2a8ffpathTff7b72.Td2a8ffisfileTb4b4b4(Te6edf3pio_esptoolTb4b4b4) Tff7b72and Te6edf3has_pyserialTb4b4b4:
Tff7b72return Tb4b4b4[Te6edf3sysTff7b72.Td2a8ffexecutableTb4b4b4, Te6edf3pio_esptoolTb4b4b4]
T8b949e# 4. Bundled exists but pyserial is missing — tell the user
Tff7b72if Te6edf3osTff7b72.Td2a8ffpathTff7b72.Td2a8ffisfileTb4b4b4(Te6edf3bundledTb4b4b4) Tff7b72and Tff7b72not Te6edf3has_pyserialTb4b4b4:
Tffa657printTb4b4b4(Ta5d6ff"Ta5d6ffFound bundled esptool but pyserial is not installed.Ta5d6ff"Tb4b4b4)
Tffa657printTb4b4b4(Ta5d6ff"Ta5d6ffInstall it with: pip install pyserialTa5d6ff"Tb4b4b4)
Tffa657printTb4b4b4(Ta5d6ff"Ta5d6ffOr install the standalone esptool: pip install esptoolTa5d6ff"Tb4b4b4)
Te6edf3sysTff7b72.Td2a8ffexitTb4b4b4(T79c0ff1Tb4b4b4)
Tff7b72return Tff7b72None
Tff7b72def Td2a8fffind_serial_portTb4b4b4(Tb4b4b4)Tb4b4b4:
T8b949e"""List available serial ports and let the user choose."""
Te6edf3system Tff7b72= Te6edf3platformTff7b72.Td2a8ffsystemTb4b4b4(Tb4b4b4)
T8b949e# Gather ports from glob patterns
Tff7b72if Te6edf3system Tff7b72== Ta5d6ff"Ta5d6ffDarwinTa5d6ff"Tb4b4b4:
Te6edf3patterns Tff7b72= Tb4b4b4[Ta5d6ff"Ta5d6ff/dev/cu.usbmodem*Ta5d6ff"Tb4b4b4, Ta5d6ff"Ta5d6ff/dev/tty.usbmodem*Ta5d6ff"Tb4b4b4,
Ta5d6ff"Ta5d6ff/dev/cu.usbserial*Ta5d6ff"Tb4b4b4, Ta5d6ff"Ta5d6ff/dev/cu.SLAB*Ta5d6ff"Tb4b4b4]
Tff7b72elif Te6edf3system Tff7b72== Ta5d6ff"Ta5d6ffLinuxTa5d6ff"Tb4b4b4:
Te6edf3patterns Tff7b72= Tb4b4b4[Ta5d6ff"Ta5d6ff/dev/ttyACM*Ta5d6ff"Tb4b4b4, Ta5d6ff"Ta5d6ff/dev/ttyUSB*Ta5d6ff"Tb4b4b4]
Tff7b72elseTb4b4b4:
Te6edf3patterns Tff7b72= Tb4b4b4[Tb4b4b4]
Te6edf3ports Tff7b72= Tb4b4b4[Tb4b4b4]
Tff7b72for Te6edf3pattern Tff7b72in Te6edf3patternsTb4b4b4:
Te6edf3portsTff7b72.Td2a8ffextendTb4b4b4(Te6edf3globTff7b72.Td2a8ffglobTb4b4b4(Te6edf3patternTb4b4b4)Tb4b4b4)
T8b949e# Also try pyserial's port enumeration (works on all platforms including Windows)
Tff7b72tryTb4b4b4:
Tff7b72import T7ee787serialT7ee787.T7ee787toolsT7ee787.T7ee787list_ports
Tff7b72for Te6edf3port Tff7b72in Te6edf3serialTff7b72.Td2a8fftoolsTff7b72.Td2a8fflist_portsTff7b72.Td2a8ffcomportsTb4b4b4(Tb4b4b4)Tb4b4b4:
Tff7b72if Te6edf3portTff7b72.Td2a8ffdevice Tff7b72not Tff7b72in Te6edf3portsTb4b4b4:
Te6edf3portsTff7b72.Td2a8ffappendTb4b4b4(Te6edf3portTff7b72.Td2a8ffdeviceTb4b4b4)
Tff7b72except Tf85149ImportErrorTb4b4b4:
Tff7b72pass
T8b949e# Sort for consistent ordering
Te6edf3portsTff7b72.Td2a8ffsortTb4b4b4(Tb4b4b4)
Tff7b72if Tff7b72not Te6edf3portsTb4b4b4:
Tff7b72return Tff7b72None
Tffa657printTb4b4b4(Ta5d6ff"Tffea00\nTa5d6ffAvailable serial ports:Ta5d6ff"Tb4b4b4)
Tff7b72for Te6edf3iTb4b4b4, Te6edf3p Tff7b72in Tffa657enumerateTb4b4b4(Te6edf3portsTb4b4b4)Tb4b4b4:
Tffa657printTb4b4b4(Ta5d6fffTa5d6ff"Ta5d6ff [Tffd700{Te6edf3iTff7b72+T79c0ff1Tffd700}Ta5d6ff] Tffd700{Te6edf3pTffd700}Ta5d6ff"Tb4b4b4)
Tffa657printTb4b4b4(Tb4b4b4)
Tff7b72while Tff7b72TrueTb4b4b4:
Tff7b72tryTb4b4b4:
Te6edf3choice Tff7b72= Tffa657inputTb4b4b4(Ta5d6fffTa5d6ff"Ta5d6ffSelect port [1-Tffd700{Tffa657lenTb4b4b4(Te6edf3portsTb4b4b4)Tffd700}Ta5d6ff]: Ta5d6ff"Tb4b4b4)Tff7b72.Td2a8ffstripTb4b4b4(Tb4b4b4)
Te6edf3idx Tff7b72= Tffa657intTb4b4b4(Te6edf3choiceTb4b4b4) Tff7b72- T79c0ff1
Tff7b72if T79c0ff0 Tff7b72<Tff7b72= Te6edf3idx Tff7b72< Tffa657lenTb4b4b4(Te6edf3portsTb4b4b4)Tb4b4b4:
Tff7b72return Te6edf3portsTb4b4b4[Te6edf3idxTb4b4b4]
Tff7b72except Tb4b4b4(Tf85149ValueErrorTb4b4b4, Tf85149EOFErrorTb4b4b4)Tb4b4b4:
Tff7b72pass
Tffa657printTb4b4b4(Ta5d6ff"Ta5d6ffInvalid selection, try again.Ta5d6ff"Tb4b4b4)
Tff7b72def Td2a8ffsha256_fileTb4b4b4(Te6edf3pathTb4b4b4)Tb4b4b4:
T8b949e"""Compute SHA-256 hash of a file."""
Te6edf3h Tff7b72= Te6edf3hashlibTff7b72.Td2a8ffsha256Tb4b4b4(Tb4b4b4)
Tff7b72with Tffa657openTb4b4b4(Te6edf3pathTb4b4b4, Ta5d6ff"Ta5d6ffrbTa5d6ff"Tb4b4b4) Tff7b72as Te6edf3fTb4b4b4:
Tff7b72for Te6edf3chunk Tff7b72in Tffa657iterTb4b4b4(Tff7b72lambdaTb4b4b4: Te6edf3fTff7b72.Td2a8ffreadTb4b4b4(T79c0ff65536Tb4b4b4)Tb4b4b4, Ta5d6ffbTa5d6ff"Ta5d6ff"Tb4b4b4)Tb4b4b4:
Te6edf3hTff7b72.Td2a8ffupdateTb4b4b4(Te6edf3chunkTb4b4b4)
Tff7b72return Te6edf3hTff7b72.Td2a8ffhexdigestTb4b4b4(Tb4b4b4)
Tff7b72def Td2a8ffdownload_firmwareTb4b4b4(Te6edf3dest_pathTb4b4b4)Tb4b4b4:
T8b949e"""Download the latest merged firmware from GitHub Releases."""
Tff7b72tryTb4b4b4:
Tff7b72from T7ee787urllibT7ee787.T7ee787request Tff7b72import Te6edf3urlretrieveTb4b4b4, Te6edf3urlopen
Tff7b72import T7ee787json
Tff7b72except Tf85149ImportErrorTb4b4b4:
Tffa657printTb4b4b4(Ta5d6ff"Ta5d6ffError: Python urllib not available.Ta5d6ff"Tb4b4b4)
Tff7b72return Tff7b72False
Te6edf3api_url Tff7b72= Ta5d6fffTa5d6ff"Ta5d6ffhttps://api.github.com/repos/Tffd700{Te6edf3GITHUB_REPOTffd700}Ta5d6ff/releases/latestTa5d6ff"
Tffa657printTb4b4b4(Ta5d6fffTa5d6ff"Ta5d6ffChecking latest release from Tffd700{Te6edf3GITHUB_REPOTffd700}Ta5d6ff...Ta5d6ff"Tb4b4b4)
Tff7b72tryTb4b4b4:
Tff7b72with Te6edf3urlopenTb4b4b4(Te6edf3api_urlTb4b4b4) Tff7b72as Te6edf3respTb4b4b4:
Te6edf3release Tff7b72= Te6edf3jsonTff7b72.Td2a8ffloadsTb4b4b4(Te6edf3respTff7b72.Td2a8ffreadTb4b4b4(Tb4b4b4)Tb4b4b4)
Tff7b72except Tf85149Exception Tff7b72as Te6edf3eTb4b4b4:
Tffa657printTb4b4b4(Ta5d6fffTa5d6ff"Ta5d6ffError fetching release info: Tffd700{Te6edf3eTffd700}Ta5d6ff"Tb4b4b4)
Tff7b72return Tff7b72False
T8b949e# Find the merged firmware asset
Te6edf3asset_url Tff7b72= Tff7b72None
Tff7b72for Te6edf3asset Tff7b72in Te6edf3releaseTff7b72.Td2a8ffgetTb4b4b4(Ta5d6ff"Ta5d6ffassetsTa5d6ff"Tb4b4b4, Tb4b4b4[Tb4b4b4]Tb4b4b4)Tb4b4b4:
Tff7b72if Te6edf3assetTb4b4b4[Ta5d6ff"Ta5d6ffnameTa5d6ff"Tb4b4b4] Tff7b72== Te6edf3MERGED_FILENAMETb4b4b4(Tb4b4b4)Tb4b4b4:
Te6edf3asset_url Tff7b72= Te6edf3assetTb4b4b4[Ta5d6ff"Ta5d6ffbrowser_download_urlTa5d6ff"Tb4b4b4]
Tff7b72break
Tff7b72if Tff7b72not Te6edf3asset_urlTb4b4b4:
Tffa657printTb4b4b4(Ta5d6fffTa5d6ff"Ta5d6ffError: Ta5d6ff'Tffd700{Te6edf3MERGED_FILENAMETb4b4b4(Tb4b4b4)Tffd700}Ta5d6ff'Ta5d6ff not found in latest release (Tffd700{Te6edf3releaseTff7b72.Td2a8ffgetTb4b4b4(Ta5d6ff'Ta5d6fftag_nameTa5d6ff'Tb4b4b4, Ta5d6ff'Ta5d6ff?Ta5d6ff'Tb4b4b4)Tffd700}Ta5d6ff).Ta5d6ff"Tb4b4b4)
Tffa657printTb4b4b4(Ta5d6ff"Ta5d6ffAvailable assets:Ta5d6ff"Tb4b4b4, Tb4b4b4[Te6edf3aTb4b4b4[Ta5d6ff"Ta5d6ffnameTa5d6ff"Tb4b4b4] Tff7b72for Te6edf3a Tff7b72in Te6edf3releaseTff7b72.Td2a8ffgetTb4b4b4(Ta5d6ff"Ta5d6ffassetsTa5d6ff"Tb4b4b4, Tb4b4b4[Tb4b4b4]Tb4b4b4)Tb4b4b4]Tb4b4b4)
Tff7b72return Tff7b72False
Tffa657printTb4b4b4(Ta5d6fffTa5d6ff"Ta5d6ffDownloading Tffd700{Te6edf3releaseTb4b4b4[Ta5d6ff'Ta5d6fftag_nameTa5d6ff'Tb4b4b4]Tffd700}Ta5d6ff / Tffd700{Te6edf3MERGED_FILENAMETb4b4b4(Tb4b4b4)Tffd700}Ta5d6ff...Ta5d6ff"Tb4b4b4)
Tff7b72tryTb4b4b4:
Te6edf3urlretrieveTb4b4b4(Te6edf3asset_urlTb4b4b4, Te6edf3dest_pathTb4b4b4)
Tff7b72except Tf85149Exception Tff7b72as Te6edf3eTb4b4b4:
Tffa657printTb4b4b4(Ta5d6fffTa5d6ff"Ta5d6ffDownload failed: Tffd700{Te6edf3eTffd700}Ta5d6ff"Tb4b4b4)
Tff7b72return Tff7b72False
Te6edf3size Tff7b72= Te6edf3osTff7b72.Td2a8ffpathTff7b72.Td2a8ffgetsizeTb4b4b4(Te6edf3dest_pathTb4b4b4)
Tffa657printTb4b4b4(Ta5d6fffTa5d6ff"Ta5d6ffDownloaded Tffd700{Te6edf3sizeTffd700:Ta5d6ff,Tffd700}Ta5d6ff bytes SHA-256: Tffd700{Te6edf3sha256_fileTb4b4b4(Te6edf3dest_pathTb4b4b4)Tb4b4b4[Tb4b4b4:T79c0ff16Tb4b4b4]Tffd700}Ta5d6ff...Ta5d6ff"Tb4b4b4)
Tff7b72return Tff7b72True
Tff7b72def Td2a8ff_do_mergeTb4b4b4(Te6edf3output_pathTb4b4b4, Te6edf3esptool_cmdTb4b4b4, Te6edf3bootloaderTb4b4b4, Te6edf3partitionsTb4b4b4, Te6edf3boot_app0Tb4b4b4, Te6edf3firmwareTb4b4b4)Tb4b4b4:
T8b949e"""Low-level merge: combine the four components into a single binary."""
Tffa657printTb4b4b4(Ta5d6ff"Ta5d6ffMerging firmware components...Ta5d6ff"Tb4b4b4)
Tffa657printTb4b4b4(Ta5d6fffTa5d6ff"Ta5d6ff Bootloader: Tffd700{Te6edf3bootloaderTffd700}Ta5d6ff @ 0xTffd700{Te6edf3BOOTLOADER_ADDRTffd700:Ta5d6ff04xTffd700}Ta5d6ff"Tb4b4b4)
Tffa657printTb4b4b4(Ta5d6fffTa5d6ff"Ta5d6ff Partitions: Tffd700{Te6edf3partitionsTffd700}Ta5d6ff @ 0xTffd700{Te6edf3PARTITIONS_ADDRTffd700:Ta5d6ff04xTffd700}Ta5d6ff"Tb4b4b4)
Tffa657printTb4b4b4(Ta5d6fffTa5d6ff"Ta5d6ff boot_app0: Tffd700{Te6edf3boot_app0Tffd700}Ta5d6ff @ 0xTffd700{Te6edf3BOOT_APP0_ADDRTffd700:Ta5d6ff04xTffd700}Ta5d6ff"Tb4b4b4)
Tffa657printTb4b4b4(Ta5d6fffTa5d6ff"Ta5d6ff Firmware: Tffd700{Te6edf3firmwareTffd700}Ta5d6ff @ 0xTffd700{Te6edf3APP_ADDRTffd700:Ta5d6ff05xTffd700}Ta5d6ff"Tb4b4b4)
Te6edf3cmd Tff7b72= Te6edf3esptool_cmd Tff7b72+ Tb4b4b4[
Ta5d6ff"Ta5d6ff--chipTa5d6ff"Tb4b4b4, Te6edf3CHIPTb4b4b4,
Ta5d6ff"Ta5d6ffmerge_binTa5d6ff"Tb4b4b4,
Ta5d6ff"Ta5d6ff--flash_modeTa5d6ff"Tb4b4b4, Te6edf3FLASH_MODETb4b4b4,
Ta5d6ff"Ta5d6ff--flash_freqTa5d6ff"Tb4b4b4, Te6edf3FLASH_FREQTb4b4b4,
Ta5d6ff"Ta5d6ff--flash_sizeTa5d6ff"Tb4b4b4, Te6edf3FLASH_SIZETb4b4b4(Tb4b4b4)Tb4b4b4,
Ta5d6ff"Ta5d6ff-oTa5d6ff"Tb4b4b4, Te6edf3output_pathTb4b4b4,
Ta5d6fffTa5d6ff"Ta5d6ff0xTffd700{Te6edf3BOOTLOADER_ADDRTffd700:Ta5d6ffxTffd700}Ta5d6ff"Tb4b4b4, Te6edf3bootloaderTb4b4b4,
Ta5d6fffTa5d6ff"Ta5d6ff0xTffd700{Te6edf3PARTITIONS_ADDRTffd700:Ta5d6ffxTffd700}Ta5d6ff"Tb4b4b4, Te6edf3partitionsTb4b4b4,
Ta5d6fffTa5d6ff"Ta5d6ff0xTffd700{Te6edf3BOOT_APP0_ADDRTffd700:Ta5d6ffxTffd700}Ta5d6ff"Tb4b4b4, Te6edf3boot_app0Tb4b4b4,
Ta5d6fffTa5d6ff"Ta5d6ff0xTffd700{Te6edf3APP_ADDRTffd700:Ta5d6ffxTffd700}Ta5d6ff"Tb4b4b4, Te6edf3firmwareTb4b4b4,
Tb4b4b4]
Te6edf3result Tff7b72= Te6edf3subprocessTff7b72.Td2a8ffrunTb4b4b4(Te6edf3cmdTb4b4b4, Te6edf3capture_outputTff7b72=Tff7b72TrueTb4b4b4, Te6edf3textTff7b72=Tff7b72TrueTb4b4b4)
Tff7b72if Te6edf3resultTff7b72.Td2a8ffreturncode Tff7b72!= T79c0ff0Tb4b4b4:
Tffa657printTb4b4b4(Ta5d6fffTa5d6ff"Ta5d6ffError merging: Tffd700{Te6edf3resultTff7b72.Td2a8ffstderrTffd700}Tffd700{Te6edf3resultTff7b72.Td2a8ffstdoutTffd700}Ta5d6ff"Tb4b4b4)
Tff7b72return Tff7b72False
Te6edf3size Tff7b72= Te6edf3osTff7b72.Td2a8ffpathTff7b72.Td2a8ffgetsizeTb4b4b4(Te6edf3output_pathTb4b4b4)
Tffa657printTb4b4b4(Ta5d6fffTa5d6ff"Tffea00\nTa5d6ffMerged binary: Tffd700{Te6edf3output_pathTffd700}Ta5d6ff (Tffd700{Te6edf3sizeTffd700:Ta5d6ff,Tffd700}Ta5d6ff bytes)Ta5d6ff"Tb4b4b4)
Tffa657printTb4b4b4(Ta5d6fffTa5d6ff"Ta5d6ffSHA-256: Tffd700{Te6edf3sha256_fileTb4b4b4(Te6edf3output_pathTb4b4b4)Tb4b4b4[Tb4b4b4:T79c0ff16Tb4b4b4]Tffd700}Ta5d6ff...Ta5d6ff"Tb4b4b4)
Tff7b72return Tff7b72True
Tff7b72def Td2a8ffmerge_firmwareTb4b4b4(Te6edf3output_pathTb4b4b4, Te6edf3esptool_cmdTb4b4b4)Tb4b4b4:
T8b949e"""Merge bootloader + partitions + boot_app0 + app into a single binary.
Uses PlatformIO build output, falling back to bundled Release/ copies
for the boot components.
"""
Te6edf3bootloader Tff7b72= Te6edf3find_bootloaderTb4b4b4(Tb4b4b4)
Te6edf3partitions Tff7b72= Te6edf3find_partitionsTb4b4b4(Tb4b4b4)
Te6edf3boot_app0 Tff7b72= Te6edf3BOOT_APP0_BIN
Te6edf3firmware Tff7b72= Te6edf3FIRMWARE_BINTb4b4b4(Tb4b4b4)
Te6edf3missing Tff7b72= Tb4b4b4[Tb4b4b4]
Tff7b72if Tff7b72not Te6edf3bootloaderTb4b4b4: Te6edf3missingTff7b72.Td2a8ffappendTb4b4b4(Tb4b4b4(Ta5d6ff"Ta5d6ffbootloaderTa5d6ff"Tb4b4b4, Te6edf3BOOTLOADER_BINTb4b4b4(Tb4b4b4)Tb4b4b4)Tb4b4b4)
Tff7b72if Tff7b72not Te6edf3partitionsTb4b4b4: Te6edf3missingTff7b72.Td2a8ffappendTb4b4b4(Tb4b4b4(Ta5d6ff"Ta5d6ffpartitionsTa5d6ff"Tb4b4b4, Te6edf3PARTITIONS_BINTb4b4b4(Tb4b4b4)Tb4b4b4)Tb4b4b4)
Tff7b72if Tff7b72not Te6edf3boot_app0Tb4b4b4: Te6edf3missingTff7b72.Td2a8ffappendTb4b4b4(Tb4b4b4(Ta5d6ff"Ta5d6ffboot_app0Ta5d6ff"Tb4b4b4, Ta5d6ff"Ta5d6ff(not found)Ta5d6ff"Tb4b4b4)Tb4b4b4)
Tff7b72if Tff7b72not Te6edf3osTff7b72.Td2a8ffpathTff7b72.Td2a8ffisfileTb4b4b4(Te6edf3firmwareTb4b4b4)Tb4b4b4:
Te6edf3missingTff7b72.Td2a8ffappendTb4b4b4(Tb4b4b4(Ta5d6ff"Ta5d6fffirmwareTa5d6ff"Tb4b4b4, Te6edf3firmwareTb4b4b4)Tb4b4b4)
Tff7b72if Te6edf3missingTb4b4b4:
Tff7b72for Te6edf3nameTb4b4b4, Te6edf3path Tff7b72in Te6edf3missingTb4b4b4:
Tffa657printTb4b4b4(Ta5d6fffTa5d6ff"Ta5d6ffError: Tffd700{Te6edf3nameTffd700}Ta5d6ff not found: Tffd700{Te6edf3pathTffd700}Ta5d6ff"Tb4b4b4)
Tffa657printTb4b4b4(Ta5d6fffTa5d6ff"Ta5d6ffRun Ta5d6ff'Ta5d6ffpio run -e Tffd700{Te6edf3PIO_ENVTb4b4b4(Tb4b4b4)Tffd700}Ta5d6ff'Ta5d6ff to build first.Ta5d6ff"Tb4b4b4)
Tff7b72return Tff7b72False
Tff7b72return Te6edf3_do_mergeTb4b4b4(Te6edf3output_pathTb4b4b4, Te6edf3esptool_cmdTb4b4b4, Te6edf3bootloaderTb4b4b4, Te6edf3partitionsTb4b4b4, Te6edf3boot_app0Tb4b4b4, Te6edf3firmwareTb4b4b4)
Tff7b72def Td2a8ffauto_merge_app_binaryTb4b4b4(Te6edf3app_binary_pathTb4b4b4, Te6edf3esptool_cmdTb4b4b4)Tb4b4b4:
T8b949e"""Auto-merge an app-only binary with boot components for a full flash.
Finds bootloader, partitions, and boot_app0 from PlatformIO build output
or the bundled Release/ directory, then merges them with the supplied
app binary into a temporary merged file.
Returns the path to the merged binary on success, or None on failure.
"""
Te6edf3bootloader Tff7b72= Te6edf3find_bootloaderTb4b4b4(Tb4b4b4)
Te6edf3partitions Tff7b72= Te6edf3find_partitionsTb4b4b4(Tb4b4b4)
Te6edf3boot_app0 Tff7b72= Te6edf3BOOT_APP0_BIN
Te6edf3missing Tff7b72= Tb4b4b4[Tb4b4b4]
Tff7b72if Tff7b72not Te6edf3bootloaderTb4b4b4: Te6edf3missingTff7b72.Td2a8ffappendTb4b4b4(Ta5d6ff"Ta5d6ffbootloader.binTa5d6ff"Tb4b4b4)
Tff7b72if Tff7b72not Te6edf3partitionsTb4b4b4: Te6edf3missingTff7b72.Td2a8ffappendTb4b4b4(Ta5d6ff"Ta5d6ffpartitions.binTa5d6ff"Tb4b4b4)
Tff7b72if Tff7b72not Te6edf3boot_app0Tb4b4b4: Te6edf3missingTff7b72.Td2a8ffappendTb4b4b4(Ta5d6ff"Ta5d6ffboot_app0.binTa5d6ff"Tb4b4b4)
Tff7b72if Te6edf3missingTb4b4b4:
Tffa657printTb4b4b4(Ta5d6fffTa5d6ff"Ta5d6ffCannot auto-merge: missing Tffd700{Ta5d6ff'Ta5d6ff, Ta5d6ff'Tff7b72.Td2a8ffjoinTb4b4b4(Te6edf3missingTb4b4b4)Tffd700}Ta5d6ff"Tb4b4b4)
Tffa657printTb4b4b4(Ta5d6ff"Ta5d6ffPlace them in the Release/ folder alongside flash.py, orTa5d6ff"Tb4b4b4)
Tffa657printTb4b4b4(Ta5d6fffTa5d6ff"Ta5d6ffbuild with PlatformIO: pio run -e Tffd700{Te6edf3PIO_ENVTb4b4b4(Tb4b4b4)Tffd700}Ta5d6ff"Tb4b4b4)
Tff7b72return Tff7b72None
T8b949e# Create merged binary next to the app binary
Te6edf3baseTb4b4b4, Te6edf3ext Tff7b72= Te6edf3osTff7b72.Td2a8ffpathTff7b72.Td2a8ffsplitextTb4b4b4(Te6edf3app_binary_pathTb4b4b4)
Te6edf3merged_path Tff7b72= Ta5d6fffTa5d6ff"Tffd700{Te6edf3baseTffd700}Ta5d6ff_mergedTffd700{Te6edf3extTffd700}Ta5d6ff"
Tffa657printTb4b4b4(Ta5d6ff"Ta5d6ffAuto-merging app-only binary with boot components...Ta5d6ff"Tb4b4b4)
Tff7b72if Te6edf3_do_mergeTb4b4b4(Te6edf3merged_pathTb4b4b4, Te6edf3esptool_cmdTb4b4b4, Te6edf3bootloaderTb4b4b4, Te6edf3partitionsTb4b4b4, Te6edf3boot_app0Tb4b4b4, Te6edf3app_binary_pathTb4b4b4)Tb4b4b4:
Tff7b72return Te6edf3merged_path
Tff7b72return Tff7b72None
Tff7b72def Td2a8ffreset_to_bootloaderTb4b4b4(Te6edf3portTb4b4b4)Tb4b4b4:
T8b949e"""Open serial port at 1200 baud to trigger ESP32-S3 USB bootloader reset.
Many ESP32-S3 boards with native USB will enter download mode when
the port is opened at 1200 baud with DTR toggled. This is useful
when the device is stuck or unresponsive to normal esptool connection.
"""
Tff7b72tryTb4b4b4:
Tff7b72import T7ee787serial
Tff7b72except Tf85149ImportErrorTb4b4b4:
Tffa657printTb4b4b4(Ta5d6ff"Ta5d6ffError: pyserial is required for 1200 baud reset.Ta5d6ff"Tb4b4b4)
Tffa657printTb4b4b4(Ta5d6ff"Ta5d6ffInstall it with: pip install pyserialTa5d6ff"Tb4b4b4)
Tff7b72return Tff7b72False
Tffa657printTb4b4b4(Ta5d6fffTa5d6ff"Ta5d6ffOpening Tffd700{Te6edf3portTffd700}Ta5d6ff at 1200 baud to trigger bootloader...Ta5d6ff"Tb4b4b4)
Tff7b72tryTb4b4b4:
Te6edf3ser Tff7b72= Te6edf3serialTff7b72.Td2a8ffSerialTb4b4b4(Te6edf3portTb4b4b4, T79c0ff1200Tb4b4b4)
Te6edf3serTff7b72.Td2a8ffdtr Tff7b72= Tff7b72False
Te6edf3timeTff7b72.Td2a8ffsleepTb4b4b4(T79c0ff0.1Tb4b4b4)
Te6edf3serTff7b72.Td2a8ffdtr Tff7b72= Tff7b72True
Te6edf3timeTff7b72.Td2a8ffsleepTb4b4b4(T79c0ff0.1Tb4b4b4)
Te6edf3serTff7b72.Td2a8ffdtr Tff7b72= Tff7b72False
Te6edf3serTff7b72.Td2a8ffcloseTb4b4b4(Tb4b4b4)
Tff7b72except Tf85149Exception Tff7b72as Te6edf3eTb4b4b4:
Tffa657printTb4b4b4(Ta5d6fffTa5d6ff"Ta5d6ffError: Tffd700{Te6edf3eTffd700}Ta5d6ff"Tb4b4b4)
Tff7b72return Tff7b72False
Tffa657printTb4b4b4(Ta5d6ff"Ta5d6ffWaiting for device to re-enumerate in download mode...Ta5d6ff"Tb4b4b4)
Te6edf3timeTff7b72.Td2a8ffsleepTb4b4b4(T79c0ff3Tb4b4b4)
Tffa657printTb4b4b4(Ta5d6ff"Ta5d6ffDone. The device should now be in download mode.Ta5d6ff"Tb4b4b4)
Tff7b72return Tff7b72True
Tff7b72def Td2a8ffflash_firmwareTb4b4b4(Te6edf3firmware_pathTb4b4b4, Te6edf3portTb4b4b4, Te6edf3esptool_cmdTb4b4b4, Te6edf3baudTff7b72=Tff7b72NoneTb4b4b4)Tb4b4b4:
T8b949e"""Flash firmware to the device."""
Tff7b72if Te6edf3baud Tff7b72is Tff7b72NoneTb4b4b4:
Te6edf3baud Tff7b72= Te6edf3BAUD_RATETb4b4b4(Tb4b4b4)
Te6edf3flash_size Tff7b72= Te6edf3FLASH_SIZETb4b4b4(Tb4b4b4)
Tffa657printTb4b4b4(Ta5d6fffTa5d6ff"Tffea00\nTa5d6ffFlashing Tffd700{Te6edf3firmware_pathTffd700}Ta5d6ff to Tffd700{Te6edf3portTffd700}Ta5d6ff...Ta5d6ff"Tb4b4b4)
Tffa657printTb4b4b4(Ta5d6fffTa5d6ff"Ta5d6ff Chip: Tffd700{Te6edf3CHIPTffd700}Ta5d6ff Baud: Tffd700{Te6edf3baudTffd700}Ta5d6ff Flash: Tffd700{Te6edf3flash_sizeTffd700}Tffea00\nTa5d6ff"Tb4b4b4)
T8b949e# Determine if this is a merged binary (flash at 0x0) or app-only (flash at 0x10000)
Te6edf3is_merged Tff7b72= Te6edf3is_merged_binaryTb4b4b4(Te6edf3firmware_pathTb4b4b4)
Tff7b72if Te6edf3is_mergedTb4b4b4:
Te6edf3flash_addr Tff7b72= Ta5d6fffTa5d6ff"Ta5d6ff0xTffd700{Te6edf3BOOTLOADER_ADDRTffd700:Ta5d6ffxTffd700}Ta5d6ff"
Tffa657printTb4b4b4(Ta5d6fffTa5d6ff"Ta5d6ff Detected: merged binary (partition table at 0x8000) -> flash at Tffd700{Te6edf3flash_addrTffd700}Ta5d6ff"Tb4b4b4)
Tff7b72elseTb4b4b4:
Te6edf3flash_addr Tff7b72= Ta5d6fffTa5d6ff"Ta5d6ff0xTffd700{Te6edf3APP_ADDRTffd700:Ta5d6ffxTffd700}Ta5d6ff"
Tffa657printTb4b4b4(Ta5d6fffTa5d6ff"Ta5d6ff Detected: app-only binary -> flash at Tffd700{Te6edf3flash_addrTffd700}Ta5d6ff"Tb4b4b4)
Te6edf3cmd Tff7b72= Te6edf3esptool_cmd Tff7b72+ Tb4b4b4[
Ta5d6ff"Ta5d6ff--chipTa5d6ff"Tb4b4b4, Te6edf3CHIPTb4b4b4,
Ta5d6ff"Ta5d6ff--portTa5d6ff"Tb4b4b4, Te6edf3portTb4b4b4,
Ta5d6ff"Ta5d6ff--baudTa5d6ff"Tb4b4b4, Te6edf3baudTb4b4b4,
Ta5d6ff"Ta5d6ff--beforeTa5d6ff"Tb4b4b4, Ta5d6ff"Ta5d6ffdefault_resetTa5d6ff"Tb4b4b4,
Ta5d6ff"Ta5d6ff--afterTa5d6ff"Tb4b4b4, Ta5d6ff"Ta5d6ffhard_resetTa5d6ff"Tb4b4b4,
Ta5d6ff"Ta5d6ffwrite_flashTa5d6ff"Tb4b4b4,
Ta5d6ff"Ta5d6ff-zTa5d6ff"Tb4b4b4,
Ta5d6ff"Ta5d6ff--flash_modeTa5d6ff"Tb4b4b4, Te6edf3FLASH_MODETb4b4b4,
Ta5d6ff"Ta5d6ff--flash_freqTa5d6ff"Tb4b4b4, Te6edf3FLASH_FREQTb4b4b4,
Ta5d6ff"Ta5d6ff--flash_sizeTa5d6ff"Tb4b4b4, Te6edf3flash_sizeTb4b4b4,
Te6edf3flash_addrTb4b4b4, Te6edf3firmware_pathTb4b4b4,
Tb4b4b4]
Tffa657printTb4b4b4(Ta5d6ff"Ta5d6ffRunning: Ta5d6ff" Tff7b72+ Ta5d6ff"Ta5d6ff Ta5d6ff"Tff7b72.Td2a8ffjoinTb4b4b4(Te6edf3cmdTb4b4b4[Tff7b72-T79c0ff8Tb4b4b4:Tb4b4b4]Tb4b4b4)Tb4b4b4)
Te6edf3result Tff7b72= Te6edf3subprocessTff7b72.Td2a8ffrunTb4b4b4(Te6edf3cmdTb4b4b4)
Tff7b72return Te6edf3resultTff7b72.Td2a8ffreturncode Tff7b72== T79c0ff0
T8b949e# ── Main ───────────────────────────────────────────────────────────────────────
Tff7b72def Td2a8ffmainTb4b4b4(Tb4b4b4)Tb4b4b4:
Tff7b72global Te6edf3_board
Te6edf3parser Tff7b72= Te6edf3argparseTff7b72.Td2a8ffArgumentParserTb4b4b4(
Te6edf3descriptionTff7b72=Ta5d6ff"Ta5d6ffRNodeTHV4 Flash Utility — flash boundary node firmware to Heltec V3/V4Ta5d6ff"Tb4b4b4,
Te6edf3formatter_classTff7b72=Te6edf3argparseTff7b72.Td2a8ffRawDescriptionHelpFormatterTb4b4b4,
Te6edf3epilogTff7b72=Ta5d6ff"""
Ta5d6ffExamples:
Ta5d6ff python flash.py # App-only update, V4 (default)
Ta5d6ff python flash.py --board v3 # App-only update, V3
Ta5d6ff python flash.py --full # Full flash with merged binary
Ta5d6ff python flash.py --download # Download latest release and flash
Ta5d6ff python flash.py --file firmware.bin # Flash a specific file
Ta5d6ff python flash.py --merge-only # Build merged binary for release
Ta5d6ff python flash.py --port /dev/ttyACM0 # Specify serial port
Ta5d6ff python flash.py --erase --full # Erase flash, then full flash
Ta5d6ff Ta5d6ff"""Tb4b4b4,
Tb4b4b4)
Te6edf3parserTff7b72.Td2a8ffadd_argumentTb4b4b4(Ta5d6ff"Ta5d6ff--boardTa5d6ff"Tb4b4b4, Te6edf3choicesTff7b72=Tb4b4b4[Ta5d6ff"Ta5d6ffv3Ta5d6ff"Tb4b4b4, Ta5d6ff"Ta5d6ffv4Ta5d6ff"Tb4b4b4]Tb4b4b4, Te6edf3defaultTff7b72=Tff7b72NoneTb4b4b4,
Te6edf3helpTff7b72=Ta5d6ff"Ta5d6ffTarget board: v3 (Heltec V3) or v4 (Heltec V4). Ta5d6ff"
Ta5d6ff"Ta5d6ffAuto-detected from connected device if omitted.Ta5d6ff"Tb4b4b4)
Te6edf3parserTff7b72.Td2a8ffadd_argumentTb4b4b4(Ta5d6ff"Ta5d6ff--fileTa5d6ff"Tb4b4b4, Ta5d6ff"Ta5d6ff-fTa5d6ff"Tb4b4b4, Te6edf3helpTff7b72=Ta5d6ff"Ta5d6ffPath to firmware binary to flashTa5d6ff"Tb4b4b4)
Te6edf3parserTff7b72.Td2a8ffadd_argumentTb4b4b4(Ta5d6ff"Ta5d6ff--portTa5d6ff"Tb4b4b4, Ta5d6ff"Ta5d6ff-pTa5d6ff"Tb4b4b4, Te6edf3helpTff7b72=Ta5d6ff"Ta5d6ffSerial port (auto-detected if omitted)Ta5d6ff"Tb4b4b4)
Te6edf3parserTff7b72.Td2a8ffadd_argumentTb4b4b4(Ta5d6ff"Ta5d6ff--baudTa5d6ff"Tb4b4b4, Ta5d6ff"Ta5d6ff-bTa5d6ff"Tb4b4b4, Te6edf3defaultTff7b72=Tff7b72NoneTb4b4b4, Te6edf3helpTff7b72=Ta5d6ff"Ta5d6ffBaud rate (board-specific default)Ta5d6ff"Tb4b4b4)
Te6edf3parserTff7b72.Td2a8ffadd_argumentTb4b4b4(Ta5d6ff"Ta5d6ff--downloadTa5d6ff"Tb4b4b4, Ta5d6ff"Ta5d6ff-dTa5d6ff"Tb4b4b4, Te6edf3actionTff7b72=Ta5d6ff"Ta5d6ffstore_trueTa5d6ff"Tb4b4b4,
Te6edf3helpTff7b72=Ta5d6ff"Ta5d6ffDownload latest firmware from GitHub ReleasesTa5d6ff"Tb4b4b4)
Te6edf3parserTff7b72.Td2a8ffadd_argumentTb4b4b4(Ta5d6ff"Ta5d6ff--merge-onlyTa5d6ff"Tb4b4b4, Te6edf3actionTff7b72=Ta5d6ff"Ta5d6ffstore_trueTa5d6ff"Tb4b4b4,
Te6edf3helpTff7b72=Ta5d6ff"Ta5d6ffMerge PlatformIO build output into single binary, donTa5d6ff'Ta5d6fft flashTa5d6ff"Tb4b4b4)
Te6edf3parserTff7b72.Td2a8ffadd_argumentTb4b4b4(Ta5d6ff"Ta5d6ff--fullTa5d6ff"Tb4b4b4, Te6edf3actionTff7b72=Ta5d6ff"Ta5d6ffstore_trueTa5d6ff"Tb4b4b4,
Te6edf3helpTff7b72=Ta5d6ff"Ta5d6ffFlash merged binary (bootloader + partitions + app) — overwrites everythingTa5d6ff"Tb4b4b4)
Te6edf3parserTff7b72.Td2a8ffadd_argumentTb4b4b4(Ta5d6ff"Ta5d6ff--eraseTa5d6ff"Tb4b4b4, Te6edf3actionTff7b72=Ta5d6ff"Ta5d6ffstore_trueTa5d6ff"Tb4b4b4,
Te6edf3helpTff7b72=Ta5d6ff"Ta5d6ffErase entire flash before writing (implies --full)Ta5d6ff"Tb4b4b4)
Te6edf3args Tff7b72= Te6edf3parserTff7b72.Td2a8ffparse_argsTb4b4b4(Tb4b4b4)
T8b949e# Find esptool early — needed for both auto-detect and flashing
Te6edf3esptool_cmd Tff7b72= Te6edf3find_esptoolTb4b4b4(Tb4b4b4)
Tff7b72if Tff7b72not Te6edf3esptool_cmdTb4b4b4:
Tffa657printTb4b4b4(Ta5d6ff"Ta5d6ffError: esptool not found!Ta5d6ff"Tb4b4b4)
Tffa657printTb4b4b4(Ta5d6ff"Ta5d6ffInstall it with: pip install esptoolTa5d6ff"Tb4b4b4)
Te6edf3sysTff7b72.Td2a8ffexitTb4b4b4(T79c0ff1Tb4b4b4)
T8b949e# ── Board detection ─────────────────────────────────────────────────
Te6edf3detected_info Tff7b72= Tff7b72None
Te6edf3_early_port Tff7b72= Tff7b72None
Tff7b72if Te6edf3argsTff7b72.Td2a8ffboardTb4b4b4:
T8b949e# Explicit board — no detection needed
Te6edf3_board Tff7b72= Te6edf3argsTff7b72.Td2a8ffboard
Tff7b72elif Te6edf3argsTff7b72.Td2a8ffmerge_onlyTb4b4b4:
T8b949e# No device needed for merge — fall back to default
Te6edf3_board Tff7b72= Te6edf3DEFAULT_BOARD
Tffa657printTb4b4b4(Ta5d6fffTa5d6ff"Ta5d6ff(No --board specified; defaulting to Tffd700{Te6edf3DEFAULT_BOARDTffd700}Ta5d6ff for merge)Ta5d6ff"Tb4b4b4)
Tff7b72elseTb4b4b4:
T8b949e# Auto-detect from connected device
Te6edf3_early_port Tff7b72= Te6edf3argsTff7b72.Td2a8ffport Tff7b72or Te6edf3find_serial_portTb4b4b4(Tb4b4b4)
Tff7b72if Tff7b72not Te6edf3_early_portTb4b4b4:
Tffa657printTb4b4b4(Ta5d6ff"Ta5d6ffNo serial port detected and no --board specified.Ta5d6ff"Tb4b4b4)
Tffa657printTb4b4b4(Ta5d6fffTa5d6ff"Ta5d6ffDefaulting to Tffd700{Te6edf3DEFAULT_BOARDTffd700}Ta5d6ff. Specify with --board v3 or --board v4.Ta5d6ff"Tb4b4b4)
Te6edf3_board Tff7b72= Te6edf3DEFAULT_BOARD
Tff7b72elseTb4b4b4:
Tffa657printTb4b4b4(Ta5d6fffTa5d6ff"Ta5d6ffDetecting board on Tffd700{Te6edf3_early_portTffd700}Ta5d6ff...Ta5d6ff"Tb4b4b4)
Te6edf3board_keyTb4b4b4, Te6edf3info Tff7b72= Te6edf3detect_boardTb4b4b4(Te6edf3_early_portTb4b4b4, Te6edf3esptool_cmdTb4b4b4)
Tff7b72if Te6edf3board_keyTb4b4b4:
Te6edf3_board Tff7b72= Te6edf3board_key
Te6edf3detected_info Tff7b72= Te6edf3info
Tffa657printTb4b4b4(Ta5d6fffTa5d6ff"Ta5d6ff Chip: Tffd700{Te6edf3infoTff7b72.Td2a8ffgetTb4b4b4(Ta5d6ff'Ta5d6ffchipTa5d6ff'Tb4b4b4, Ta5d6ff'Ta5d6ff?Ta5d6ff'Tb4b4b4)Tffd700}Ta5d6ff"Tb4b4b4)
Tffa657printTb4b4b4(Ta5d6fffTa5d6ff"Ta5d6ff Flash: Tffd700{Te6edf3infoTff7b72.Td2a8ffgetTb4b4b4(Ta5d6ff'Ta5d6ffflash_sizeTa5d6ff'Tb4b4b4, Ta5d6ff'Ta5d6ff?Ta5d6ff'Tb4b4b4)Tffd700}Ta5d6ff"Tb4b4b4)
Tffa657printTb4b4b4(Ta5d6fffTa5d6ff"Ta5d6ff Features: Tffd700{Te6edf3infoTff7b72.Td2a8ffgetTb4b4b4(Ta5d6ff'Ta5d6fffeaturesTa5d6ff'Tb4b4b4, Ta5d6ff'Ta5d6ff?Ta5d6ff'Tb4b4b4)Tffd700}Ta5d6ff"Tb4b4b4)
Tffa657printTb4b4b4(Ta5d6fffTa5d6ff"Ta5d6ff MAC: Tffd700{Te6edf3infoTff7b72.Td2a8ffgetTb4b4b4(Ta5d6ff'Ta5d6ffmacTa5d6ff'Tb4b4b4, Ta5d6ff'Ta5d6ff?Ta5d6ff'Tb4b4b4)Tffd700}Ta5d6ff"Tb4b4b4)
Tffa657printTb4b4b4(Ta5d6fffTa5d6ff"Ta5d6ff → Detected: Tffd700{Te6edf3BOARD_PROFILESTb4b4b4[Te6edf3board_keyTb4b4b4]Tb4b4b4[Ta5d6ff'Ta5d6ffnameTa5d6ff'Tb4b4b4]Tffd700}Ta5d6ff"Tb4b4b4)
Tff7b72elseTb4b4b4:
Te6edf3reason Tff7b72= Te6edf3info T8b949e# info is the error reason when board_key is None
Tffa657printTb4b4b4(Ta5d6fffTa5d6ff"Ta5d6ff Auto-detect failed: Tffd700{Te6edf3reasonTffd700}Ta5d6ff"Tb4b4b4)
Tffa657printTb4b4b4(Ta5d6fffTa5d6ff"Ta5d6ff Defaulting to Tffd700{Te6edf3DEFAULT_BOARDTffd700}Ta5d6ff. Specify with --board v3 or --board v4.Ta5d6ff"Tb4b4b4)
Te6edf3_board Tff7b72= Te6edf3DEFAULT_BOARD
Te6edf3baud Tff7b72= Te6edf3argsTff7b72.Td2a8ffbaud Tff7b72or Te6edf3BAUD_RATETb4b4b4(Tb4b4b4)
Te6edf3bp Tff7b72= Te6edf3board_profileTb4b4b4(Tb4b4b4)
Tffa657printTb4b4b4(Tb4b4b4)
Tffa657printTb4b4b4(Ta5d6ff"Ta5d6ff╔══════════════════════════════════════════╗Ta5d6ff"Tb4b4b4)
Tffa657printTb4b4b4(Ta5d6ff"Ta5d6ff║ RNodeTHV4 Flash Utility ║Ta5d6ff"Tb4b4b4)
Tffa657printTb4b4b4(Ta5d6fffTa5d6ff"Ta5d6ff║ Tffd700{Te6edf3bpTb4b4b4[Ta5d6ff'Ta5d6ffnameTa5d6ff'Tb4b4b4]Tffd700:Ta5d6ff^40sTffd700}Ta5d6ff ║Ta5d6ff"Tb4b4b4)
Tffa657printTb4b4b4(Ta5d6ff"Ta5d6ff╚══════════════════════════════════════════╝Ta5d6ff"Tb4b4b4)
Tffa657printTb4b4b4(Tb4b4b4)
Tffa657printTb4b4b4(Ta5d6fffTa5d6ff"Ta5d6ffUsing esptool: Tffd700{Ta5d6ff'Ta5d6ff Ta5d6ff'Tff7b72.Td2a8ffjoinTb4b4b4(Te6edf3esptool_cmdTb4b4b4)Tffd700}Ta5d6ff"Tb4b4b4)
T8b949e# --erase implies --full (after erase, device needs bootloader + partitions)
Tff7b72if Te6edf3argsTff7b72.Td2a8fferaseTb4b4b4:
Te6edf3argsTff7b72.Td2a8fffull Tff7b72= Tff7b72True
T8b949e# Determine firmware file
Te6edf3firmware_path Tff7b72= Tff7b72None
Te6edf3merged_fn Tff7b72= Te6edf3MERGED_FILENAMETb4b4b4(Tb4b4b4)
Te6edf3firmware_bin Tff7b72= Te6edf3FIRMWARE_BINTb4b4b4(Tb4b4b4)
Te6edf3pio_env Tff7b72= Te6edf3PIO_ENVTb4b4b4(Tb4b4b4)
Tff7b72if Te6edf3argsTff7b72.Td2a8fffileTb4b4b4:
Te6edf3firmware_path Tff7b72= Te6edf3argsTff7b72.Td2a8fffile
Tff7b72if Tff7b72not Te6edf3osTff7b72.Td2a8ffpathTff7b72.Td2a8ffisfileTb4b4b4(Te6edf3firmware_pathTb4b4b4)Tb4b4b4:
Tffa657printTb4b4b4(Ta5d6fffTa5d6ff"Ta5d6ffError: file not found: Tffd700{Te6edf3firmware_pathTffd700}Ta5d6ff"Tb4b4b4)
Te6edf3sysTff7b72.Td2a8ffexitTb4b4b4(T79c0ff1Tb4b4b4)
Tff7b72elif Te6edf3argsTff7b72.Td2a8ffdownloadTb4b4b4:
Te6edf3firmware_path Tff7b72= Te6edf3merged_fn
Tff7b72if Tff7b72not Te6edf3download_firmwareTb4b4b4(Te6edf3firmware_pathTb4b4b4)Tb4b4b4:
Te6edf3sysTff7b72.Td2a8ffexitTb4b4b4(T79c0ff1Tb4b4b4)
Tff7b72elif Te6edf3argsTff7b72.Td2a8ffmerge_onlyTb4b4b4:
Tff7b72if Te6edf3merge_firmwareTb4b4b4(Te6edf3merged_fnTb4b4b4, Te6edf3esptool_cmdTb4b4b4)Tb4b4b4:
Tffa657printTb4b4b4(Ta5d6fffTa5d6ff"Tffea00\nTa5d6ffDone! Flash with: python flash.py --board Tffd700{Te6edf3_boardTffd700}Ta5d6ff --file Tffd700{Te6edf3merged_fnTffd700}Ta5d6ff"Tb4b4b4)
Tff7b72elseTb4b4b4:
Te6edf3sysTff7b72.Td2a8ffexitTb4b4b4(T79c0ff1Tb4b4b4)
Tff7b72return
Tff7b72elif Te6edf3argsTff7b72.Td2a8fffullTb4b4b4:
T8b949e# Full flash: use or create merged binary
Tff7b72if Te6edf3osTff7b72.Td2a8ffpathTff7b72.Td2a8ffisfileTb4b4b4(Te6edf3firmware_binTb4b4b4)Tb4b4b4:
T8b949e# Build exists — (re-)merge
Tff7b72if Te6edf3osTff7b72.Td2a8ffpathTff7b72.Td2a8ffisfileTb4b4b4(Te6edf3merged_fnTb4b4b4)Tb4b4b4:
Te6edf3build_time Tff7b72= Te6edf3osTff7b72.Td2a8ffpathTff7b72.Td2a8ffgetmtimeTb4b4b4(Te6edf3firmware_binTb4b4b4)
Te6edf3merge_time Tff7b72= Te6edf3osTff7b72.Td2a8ffpathTff7b72.Td2a8ffgetmtimeTb4b4b4(Te6edf3merged_fnTb4b4b4)
Tff7b72if Te6edf3build_time Tff7b72> Te6edf3merge_timeTb4b4b4:
Tffa657printTb4b4b4(Ta5d6ff"Ta5d6ffBuild output is newer than merged binary, re-merging...Ta5d6ff"Tb4b4b4)
Tff7b72if Tff7b72not Te6edf3merge_firmwareTb4b4b4(Te6edf3merged_fnTb4b4b4, Te6edf3esptool_cmdTb4b4b4)Tb4b4b4:
Te6edf3sysTff7b72.Td2a8ffexitTb4b4b4(T79c0ff1Tb4b4b4)
Tff7b72elseTb4b4b4:
Tffa657printTb4b4b4(Ta5d6ff"Ta5d6ffCreating merged binary from PlatformIO build output...Ta5d6ff"Tb4b4b4)
Tff7b72if Tff7b72not Te6edf3merge_firmwareTb4b4b4(Te6edf3merged_fnTb4b4b4, Te6edf3esptool_cmdTb4b4b4)Tb4b4b4:
Te6edf3sysTff7b72.Td2a8ffexitTb4b4b4(T79c0ff1Tb4b4b4)
Te6edf3firmware_path Tff7b72= Te6edf3merged_fn
Tff7b72elif Te6edf3osTff7b72.Td2a8ffpathTff7b72.Td2a8ffisfileTb4b4b4(Te6edf3merged_fnTb4b4b4)Tb4b4b4:
Te6edf3firmware_path Tff7b72= Te6edf3merged_fn
Tff7b72elseTb4b4b4:
Tffa657printTb4b4b4(Ta5d6ff"Ta5d6ffNo firmware found for full flash!Ta5d6ff"Tb4b4b4)
Tffa657printTb4b4b4(Tb4b4b4)
Tffa657printTb4b4b4(Ta5d6ff"Ta5d6ffOptions:Ta5d6ff"Tb4b4b4)
Tffa657printTb4b4b4(Ta5d6fffTa5d6ff"Ta5d6ff 1. Build with PlatformIO first: pio run -e Tffd700{Te6edf3pio_envTffd700}Ta5d6ff"Tb4b4b4)
Tffa657printTb4b4b4(Ta5d6fffTa5d6ff"Ta5d6ff 2. Download from GitHub: python flash.py --board Tffd700{Te6edf3_boardTffd700}Ta5d6ff --downloadTa5d6ff"Tb4b4b4)
Tffa657printTb4b4b4(Ta5d6fffTa5d6ff"Ta5d6ff 3. Specify a file: python flash.py --board Tffd700{Te6edf3_boardTffd700}Ta5d6ff --file <path>Ta5d6ff"Tb4b4b4)
Te6edf3sysTff7b72.Td2a8ffexitTb4b4b4(T79c0ff1Tb4b4b4)
Tff7b72elseTb4b4b4:
T8b949e# Default: app-only flash (preserves settings)
Tff7b72if Te6edf3osTff7b72.Td2a8ffpathTff7b72.Td2a8ffisfileTb4b4b4(Te6edf3firmware_binTb4b4b4)Tb4b4b4:
Te6edf3firmware_path Tff7b72= Te6edf3firmware_bin
Tffa657printTb4b4b4(Ta5d6fffTa5d6ff"Ta5d6ffApp-only update (preserves WiFi/boundary settings)Ta5d6ff"Tb4b4b4)
Tffa657printTb4b4b4(Ta5d6fffTa5d6ff"Ta5d6ff Use --full for a complete flash, or --erase for recovery.Ta5d6ff"Tb4b4b4)
Tff7b72elif Te6edf3osTff7b72.Td2a8ffpathTff7b72.Td2a8ffisfileTb4b4b4(Te6edf3merged_fnTb4b4b4)Tb4b4b4:
Te6edf3firmware_path Tff7b72= Te6edf3merged_fn
Tffa657printTb4b4b4(Ta5d6fffTa5d6ff"Ta5d6ffNo build output found, using merged binary: Tffd700{Te6edf3merged_fnTffd700}Ta5d6ff"Tb4b4b4)
Tffa657printTb4b4b4(Ta5d6fffTa5d6ff"Ta5d6ff Note: merged binary will overwrite bootloader + partitions.Ta5d6ff"Tb4b4b4)
Tff7b72elseTb4b4b4:
Tffa657printTb4b4b4(Ta5d6ff"Ta5d6ffNo firmware found!Ta5d6ff"Tb4b4b4)
Tffa657printTb4b4b4(Tb4b4b4)
Tffa657printTb4b4b4(Ta5d6ff"Ta5d6ffOptions:Ta5d6ff"Tb4b4b4)
Tffa657printTb4b4b4(Ta5d6fffTa5d6ff"Ta5d6ff 1. Build with PlatformIO first: pio run -e Tffd700{Te6edf3pio_envTffd700}Ta5d6ff"Tb4b4b4)
Tffa657printTb4b4b4(Ta5d6fffTa5d6ff"Ta5d6ff 2. Download from GitHub: python flash.py --board Tffd700{Te6edf3_boardTffd700}Ta5d6ff --downloadTa5d6ff"Tb4b4b4)
Tffa657printTb4b4b4(Ta5d6fffTa5d6ff"Ta5d6ff 3. Specify a file: python flash.py --board Tffd700{Te6edf3_boardTffd700}Ta5d6ff --file <path>Ta5d6ff"Tb4b4b4)
Te6edf3sysTff7b72.Td2a8ffexitTb4b4b4(T79c0ff1Tb4b4b4)
T8b949e# Flash — reuse early-detected port if available
Te6edf3port Tff7b72= Te6edf3argsTff7b72.Td2a8ffport Tff7b72or Te6edf3_early_port Tff7b72or Te6edf3find_serial_portTb4b4b4(Tb4b4b4)
Tff7b72if Tff7b72not Te6edf3portTb4b4b4:
Tffa657printTb4b4b4(Ta5d6ff"Tffea00\nTa5d6ffError: No serial port detected!Ta5d6ff"Tb4b4b4)
Tffa657printTb4b4b4(Ta5d6fffTa5d6ff"Ta5d6ffConnect your Tffd700{Te6edf3bpTb4b4b4[Ta5d6ff'Ta5d6ffnameTa5d6ff'Tb4b4b4]Tffd700}Ta5d6ff via USB and try again,Ta5d6ff"Tb4b4b4)
Tffa657printTb4b4b4(Ta5d6fffTa5d6ff"Ta5d6ffor specify manually: python flash.py --board Tffd700{Te6edf3_boardTffd700}Ta5d6ff --port /dev/ttyACM0Ta5d6ff"Tb4b4b4)
Te6edf3sysTff7b72.Td2a8ffexitTb4b4b4(T79c0ff1Tb4b4b4)
Tffa657printTb4b4b4(Ta5d6fffTa5d6ff"Tffea00\nTa5d6ffSerial port: Tffd700{Te6edf3portTffd700}Ta5d6ff"Tb4b4b4)
Tffa657printTb4b4b4(Ta5d6fffTa5d6ff"Ta5d6ffFirmware: Tffd700{Te6edf3firmware_pathTffd700}Ta5d6ff (Tffd700{Te6edf3osTff7b72.Td2a8ffpathTff7b72.Td2a8ffgetsizeTb4b4b4(Te6edf3firmware_pathTb4b4b4)Tffd700:Ta5d6ff,Tffd700}Ta5d6ff bytes)Ta5d6ff"Tb4b4b4)
Tffa657printTb4b4b4(Tb4b4b4)
T8b949e# ── Interactive options ─────────────────────────────────────────────────
T8b949e# Offer 1200 baud reset if device might be stuck
Tff7b72tryTb4b4b4:
Te6edf3reset_choice Tff7b72= Tffa657inputTb4b4b4(Ta5d6ff"Ta5d6ffReset device to download mode first? (try if device is stuck) [y/N] Ta5d6ff"Tb4b4b4)Tff7b72.Td2a8ffstripTb4b4b4(Tb4b4b4)Tff7b72.Td2a8fflowerTb4b4b4(Tb4b4b4)
Tff7b72except Tf85149EOFErrorTb4b4b4:
Te6edf3reset_choice Tff7b72= Ta5d6ff"Ta5d6ff"
Tff7b72if Te6edf3reset_choice Tff7b72== Ta5d6ff"Ta5d6ffyTa5d6ff"Tb4b4b4:
Te6edf3reset_to_bootloaderTb4b4b4(Te6edf3portTb4b4b4)
T8b949e# Port may change after reset — re-scan
Tffa657printTb4b4b4(Ta5d6ff"Ta5d6ffRe-scanning serial ports (port may have changed)...Ta5d6ff"Tb4b4b4)
Te6edf3new_port Tff7b72= Te6edf3argsTff7b72.Td2a8ffport Tff7b72or Te6edf3find_serial_portTb4b4b4(Tb4b4b4)
Tff7b72if Te6edf3new_portTb4b4b4:
Te6edf3port Tff7b72= Te6edf3new_port
Tffa657printTb4b4b4(Ta5d6fffTa5d6ff"Ta5d6ffUsing port: Tffd700{Te6edf3portTffd700}Ta5d6ff"Tb4b4b4)
Tff7b72elseTb4b4b4:
Tffa657printTb4b4b4(Ta5d6fffTa5d6ff"Ta5d6ffWarning: No ports found after reset. Continuing with Tffd700{Te6edf3portTffd700}Ta5d6ff"Tb4b4b4)
T8b949e# Offer erase unless --erase was already passed
Tff7b72if Tff7b72not Te6edf3argsTff7b72.Td2a8fferaseTb4b4b4:
Tff7b72tryTb4b4b4:
Te6edf3erase_choice Tff7b72= Tffa657inputTb4b4b4(Ta5d6ff"Ta5d6ffErase flash before writing? (wipes all settings) [y/N] Ta5d6ff"Tb4b4b4)Tff7b72.Td2a8ffstripTb4b4b4(Tb4b4b4)Tff7b72.Td2a8fflowerTb4b4b4(Tb4b4b4)
Tff7b72except Tf85149EOFErrorTb4b4b4:
Te6edf3erase_choice Tff7b72= Ta5d6ff"Ta5d6ff"
Tff7b72if Te6edf3erase_choice Tff7b72== Ta5d6ff"Ta5d6ffyTa5d6ff"Tb4b4b4:
Te6edf3argsTff7b72.Td2a8fferase Tff7b72= Tff7b72True
T8b949e# Erase needs bootloader+partitions, auto-merge if we have app-only
T8b949e# ── Safety check: erase + app-only → auto-merge ────────────────────────
Tff7b72if Te6edf3argsTff7b72.Td2a8fferase Tff7b72and Tff7b72not Te6edf3is_merged_binaryTb4b4b4(Te6edf3firmware_pathTb4b4b4)Tb4b4b4:
Tffa657printTb4b4b4(Tb4b4b4)
Tffa657printTb4b4b4(Ta5d6ff"Ta5d6ff╔══════════════════════════════════════════════════════════════╗Ta5d6ff"Tb4b4b4)
Tffa657printTb4b4b4(Ta5d6ff"Ta5d6ff║ Erase selected with app-only binary — auto-merging boot ║Ta5d6ff"Tb4b4b4)
Tffa657printTb4b4b4(Ta5d6ff"Ta5d6ff║ components (bootloader + partition table + boot_app0) so ║Ta5d6ff"Tb4b4b4)
Tffa657printTb4b4b4(Ta5d6ff"Ta5d6ff║ the device remains bootable after erase. ║Ta5d6ff"Tb4b4b4)
Tffa657printTb4b4b4(Ta5d6ff"Ta5d6ff╚══════════════════════════════════════════════════════════════╝Ta5d6ff"Tb4b4b4)
Tffa657printTb4b4b4(Tb4b4b4)
Te6edf3merged Tff7b72= Te6edf3auto_merge_app_binaryTb4b4b4(Te6edf3firmware_pathTb4b4b4, Te6edf3esptool_cmdTb4b4b4)
Tff7b72if Te6edf3mergedTb4b4b4:
Te6edf3firmware_path Tff7b72= Te6edf3merged
Tffa657printTb4b4b4(Ta5d6fffTa5d6ff"Tffea00\nTa5d6ffUsing auto-merged binary: Tffd700{Te6edf3firmware_pathTffd700}Ta5d6ff"Tb4b4b4)
Tffa657printTb4b4b4(Ta5d6fffTa5d6ff"Ta5d6ff Size: Tffd700{Te6edf3osTff7b72.Td2a8ffpathTff7b72.Td2a8ffgetsizeTb4b4b4(Te6edf3firmware_pathTb4b4b4)Tffd700:Ta5d6ff,Tffd700}Ta5d6ff bytesTa5d6ff"Tb4b4b4)
Tffa657printTb4b4b4(Tb4b4b4)
Tff7b72elseTb4b4b4:
Tffa657printTb4b4b4(Tb4b4b4)
Tffa657printTb4b4b4(Ta5d6ff"Ta5d6ffAuto-merge failed. Options:Ta5d6ff"Tb4b4b4)
Tffa657printTb4b4b4(Ta5d6ff"Ta5d6ff 1) Skip erase and flash app-only (preserves existing NVS/bootloader)Ta5d6ff"Tb4b4b4)
Tffa657printTb4b4b4(Ta5d6ff"Ta5d6ff 2) AbortTa5d6ff"Tb4b4b4)
Tff7b72tryTb4b4b4:
Te6edf3fallback Tff7b72= Tffa657inputTb4b4b4(Ta5d6ff"Tffea00\nTa5d6ffSkip erase and continue with app-only flash? [Y/n] Ta5d6ff"Tb4b4b4)Tff7b72.Td2a8ffstripTb4b4b4(Tb4b4b4)Tff7b72.Td2a8fflowerTb4b4b4(Tb4b4b4)
Tff7b72except Tf85149EOFErrorTb4b4b4:
Te6edf3fallback Tff7b72= Ta5d6ff"Ta5d6ff"
Tff7b72if Te6edf3fallback Tff7b72== Ta5d6ff"Ta5d6ffnTa5d6ff"Tb4b4b4:
Tffa657printTb4b4b4(Ta5d6ff"Ta5d6ffAborted.Ta5d6ff"Tb4b4b4)
Te6edf3sysTff7b72.Td2a8ffexitTb4b4b4(T79c0ff1Tb4b4b4)
Te6edf3argsTff7b72.Td2a8fferase Tff7b72= Tff7b72False
Tffa657printTb4b4b4(Ta5d6ff"Ta5d6ffErase skipped. Continuing with app-only flash...Tffea00\nTa5d6ff"Tb4b4b4)
Te6edf3confirm Tff7b72= Tffa657inputTb4b4b4(Ta5d6ff"Tffea00\nTa5d6ffFlash firmware? [Y/n] Ta5d6ff"Tb4b4b4)Tff7b72.Td2a8ffstripTb4b4b4(Tb4b4b4)Tff7b72.Td2a8fflowerTb4b4b4(Tb4b4b4)
Tff7b72if Te6edf3confirm Tff7b72and Te6edf3confirm Tff7b72!= Ta5d6ff"Ta5d6ffyTa5d6ff"Tb4b4b4:
Tffa657printTb4b4b4(Ta5d6ff"Ta5d6ffAborted.Ta5d6ff"Tb4b4b4)
Te6edf3sysTff7b72.Td2a8ffexitTb4b4b4(T79c0ff0Tb4b4b4)
Tff7b72if Te6edf3argsTff7b72.Td2a8fferaseTb4b4b4:
Tffa657printTb4b4b4(Ta5d6fffTa5d6ff"Ta5d6ffErasing flash on Tffd700{Te6edf3portTffd700}Ta5d6ff...Ta5d6ff"Tb4b4b4)
Te6edf3erase_cmd Tff7b72= Te6edf3esptool_cmd Tff7b72+ Tb4b4b4[
Ta5d6ff"Ta5d6ff--chipTa5d6ff"Tb4b4b4, Te6edf3CHIPTb4b4b4,
Ta5d6ff"Ta5d6ff--portTa5d6ff"Tb4b4b4, Te6edf3portTb4b4b4,
Ta5d6ff"Ta5d6ff--baudTa5d6ff"Tb4b4b4, Te6edf3baudTb4b4b4,
Ta5d6ff"Ta5d6fferase_flashTa5d6ff"Tb4b4b4,
Tb4b4b4]
Te6edf3result Tff7b72= Te6edf3subprocessTff7b72.Td2a8ffrunTb4b4b4(Te6edf3erase_cmdTb4b4b4)
Tff7b72if Te6edf3resultTff7b72.Td2a8ffreturncode Tff7b72!= T79c0ff0Tb4b4b4:
Tffa657printTb4b4b4(Ta5d6ff"Tffea00\nTa5d6ffErase FAILED.Ta5d6ff"Tb4b4b4)
Te6edf3sysTff7b72.Td2a8ffexitTb4b4b4(T79c0ff1Tb4b4b4)
Tffa657printTb4b4b4(Ta5d6ff"Ta5d6ffFlash erased. Waiting for device to re-enumerate...Ta5d6ff"Tb4b4b4)
Te6edf3timeTff7b72.Td2a8ffsleepTb4b4b4(T79c0ff3Tb4b4b4)
Tff7b72if Te6edf3flash_firmwareTb4b4b4(Te6edf3firmware_pathTb4b4b4, Te6edf3portTb4b4b4, Te6edf3esptool_cmdTb4b4b4, Te6edf3baudTb4b4b4)Tb4b4b4:
Tffa657printTb4b4b4(Tb4b4b4)
Tffa657printTb4b4b4(Ta5d6ff"Ta5d6ff╔══════════════════════════════════════════╗Ta5d6ff"Tb4b4b4)
Tffa657printTb4b4b4(Ta5d6ff"Ta5d6ff║ Flash complete! ║Ta5d6ff"Tb4b4b4)
Tffa657printTb4b4b4(Ta5d6ff"Ta5d6ff║ Device will reboot automatically. ║Ta5d6ff"Tb4b4b4)
Tffa657printTb4b4b4(Ta5d6ff"Ta5d6ff║ ║Ta5d6ff"Tb4b4b4)
Tffa657printTb4b4b4(Ta5d6ff"Ta5d6ff║ On first boot, hold PRG > 5s to enter ║Ta5d6ff"Tb4b4b4)
Tffa657printTb4b4b4(Ta5d6ff"Ta5d6ff║ the configuration portal. ║Ta5d6ff"Tb4b4b4)
Tffa657printTb4b4b4(Ta5d6ff"Ta5d6ff╚══════════════════════════════════════════╝Ta5d6ff"Tb4b4b4)
Tff7b72elseTb4b4b4:
Tffa657printTb4b4b4(Ta5d6ff"Tffea00\nTa5d6ffFlash FAILED. Check connection and try again.Ta5d6ff"Tb4b4b4)
Tffa657printTb4b4b4(Ta5d6ff"Ta5d6ffYou may need to hold BOOT while pressing RESET.Ta5d6ff"Tb4b4b4)
Te6edf3sysTff7b72.Td2a8ffexitTb4b4b4(T79c0ff1Tb4b4b4)
Tff7b72if Tff7b72__name__ Tff7b72== Ta5d6ff"Ta5d6ff__main__Ta5d6ff"Tb4b4b4:
Te6edf3mainTb4b4b4(Tb4b4b4)
Served by rngit 1.4.2 - Generated in 0.08s